package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"flag"
	"fmt"
	wait "github.com/asaf-shitrit/go-wait"
	"github.com/spf13/afero"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	"gitverse.ru/synapse/kubelatte/pkg/operator"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	kbltwebhook "gitverse.ru/synapse/kubelatte/pkg/webhook"
	"gitverse.ru/synapse/kubelatte/pkg/webhook/config"
	"io"
	"net/http"
	"os"
	"os/signal"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	"sync"
	"syscall"
	"time"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"k8s.io/klog/v2"

	"gitverse.ru/synapse/kubelatte/pkg/operator/controllers/clientset"
	"gitverse.ru/synapse/kubelatte/pkg/operator/leader"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	_ "k8s.io/client-go/plugin/pkg/client/auth"
	ctrl "sigs.k8s.io/controller-runtime"
	//+kubebuilder:scaffold:imports
)

var (
	scheme = runtime.NewScheme()
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(v1alpha1.AddToScheme(scheme))
	//+kubebuilder:scaffold:scheme

	configConfs := ctrl.GetConfigOrDie()
	err := v1alpha1.AddToScheme(clientgoscheme.Scheme)
	if err != nil {
		panic("adding to scheme error:" + err.Error())
		return
	}

	operator.Cl, err = clientset.NewForConfig(configConfs)
	if err != nil {
		klog.Fatal(err)
	}
}

func startPrometheusServer() {
	http.Handle("/metrics", promhttp.Handler())
	go func() {
		err := http.ListenAndServe(":2112", nil)
		logs.Fatal(fmt.Sprintf("Error start prometheus server: %s", err.Error()))
		os.Exit(1)
	}()
}

func waitConnectionToKubeAPI() {

	cfg, err := ctrl.GetConfig()
	if err != nil {
		logs.Error(err.Error())
		os.Exit(1)
	}

	t := &http.Transport{}

	if operator.WithoutCert != "true" {
		var cert tls.Certificate
		if cfg.CertFile != "" && cfg.KeyFile != "" {
			cert, err = tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
			if err != nil {
				logs.Fatal(fmt.Sprintf("Error creating x509 keypair from client cert file %s and client key file %s",
					cfg.CertFile, cfg.KeyFile))
				os.Exit(1)
			}
		}

		caCert, err := os.ReadFile(cfg.CAFile)
		if err != nil {
			logs.Fatal(fmt.Sprintf("Error opening cert file %s, Error: %s", cfg.CAFile, err))
			os.Exit(1)
		}
		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCert)

		t = &http.Transport{
			TLSClientConfig: &tls.Config{
				Certificates: []tls.Certificate{cert},
				RootCAs:      caCertPool,
			},
		}
	}

	client := http.Client{Transport: t, Timeout: 15 * time.Second}
	var bearer = "Bearer " + cfg.BearerToken
	var uri = cfg.Host + "/livez"

	checkFunc := func() (bool, error) {
		rq, err := http.NewRequest("GET", uri, nil)
		if err != nil {
			logs.Error(err.Error())
			return false, nil
		}

		if cfg.BearerToken != "" {
			rq.Header.Add("Authorization", bearer)
		}

		resp, err := client.Do(rq)

		if err != nil {
			logs.Error(err.Error())
			return false, nil
		}
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)

		if resp.StatusCode == http.StatusOK {
			logs.Infof("Kubernetes APIServer available. Status request: %d", resp.StatusCode)
			return true, nil
		}
		logs.Error(fmt.Sprintf("Kubernetes APIServer unavailable. Status request: %d. Reconecting...", resp.StatusCode))
		return false, nil
	}

	options := &wait.BackoffOptions{
		BaselineDuration: time.Second,
		Limit:            10 * time.Second,
		Multiplier:       2,
		Jitter:           1,
	}

	ctx := context.Background()
	if err = wait.Backoff(ctx, checkFunc, options); err != nil {
		logs.Infof("Unable to start operator: %s", err.Error())
		os.Exit(1)
	}
}

func main() {
	var err error

	util.GlobalInit()

	logs.LoggersInit()
	operator.PrometheusInit()
	startPrometheusServer()
	waitConnectionToKubeAPI()

	var metricsAddr string
	var enableLeaderElection bool
	var probeAddr string
	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.")
	flag.Parse()

	//start server
	go func() {
		fs := afero.NewOsFs()

		webhookConfig, cerr := config.Load()
		if cerr != nil {
			logs.Errorf("api=main, reason=webhook.NewWebhookServer, err=%v", cerr)
			wc, err := config.NewWebhookConfig()
			if err != nil {
				panic(err)
			}
			webhookConfig = wc
		}

		var certReloader *util.CertificatePKIReloader
		if webhookConfig.CertFilePath != "" && webhookConfig.KeyFilePath != "" {
			certReloader = util.NewCertificatePKIReloaderFull(fs, webhookConfig.CertFilePath, webhookConfig.KeyFilePath, time.Minute*15)
		}

		whsrv := kbltwebhook.NewWebhookServer(webhookConfig, certReloader, &sync.Mutex{})
		doneListeningTLSChannel, doneListeningHTTPChannel, err := whsrv.Start()
		if err != nil {
			logs.Errorf("api=main, reason=whsrv.Start, err=%v", err)
			panic(err)
		}

		if cerr != nil {
			whsrv.Set(kbltwebhook.ServerConfigsError)
		}

		// listening OS shutdown entrySignal
		signalChan := make(chan os.Signal, 1)
		signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)

		stopped := false
		// Wait until we receive either a termination entrySignal, or the server stops by itself for some reason
		select {
		case entrySignal := <-signalChan:
			logs.Warn("Received a termination entrySignal. SIG=", entrySignal)
		case stopped = <-doneListeningTLSChannel:
			logs.Warn("TLS Server has stopped on it's own... exiting.")
		case stopped = <-doneListeningHTTPChannel:
			logs.Warn("HTTP Server has stopped on it's own... exiting.")
		}

		if !stopped {
			whsrv.Stop()
		}
		logs.Info("Webhook server exited successfully.")

	}()

	opt := ctrl.Options{
		Logger:                 logs.LoggerSTD,
		Scheme:                 scheme,
		MetricsBindAddress:     metricsAddr,
		HealthProbeBindAddress: probeAddr,
		WebhookServer:          webhook.NewServer(webhook.Options{Port: util.IKbltMainPort}),
	}

	if operator.LocalNamespaceMode == operator.EnvBoolValueTrue {
		opt.Cache.Namespaces = append(opt.Cache.Namespaces, operator.OperatorNamespace)
	} else if operator.LocalNamespaceMode != operator.EnvBoolValueFalse {
		logs.Fatal(fmt.Sprintf("Invalid environment variable LOCAL_NAMESPACE_MODE: use 'true' or 'false'"))
		os.Exit(1)
	}

	leader.Mgr, err = ctrl.NewManager(ctrl.GetConfigOrDie(), opt)
	if err != nil {
		logs.Fatal("Unable to start manager")
		os.Exit(1)
	}

	leader.SetLeaderAndStart()
}
